package ca.uvic.group2;

import ca.uvic.group2.api.*;
import ca.uvic.group2.api.exceptions.*;
import ca.uvic.group2.server.*;
import java.net.*;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.NotBoundException;
import org.apache.log4j.*;
import java.util.*;

public class ServerFactory {
    /*
    private static TransactionServer _transactionServer;
    private static RemoteServer _remoteTransactionServer;
    private static QuoteServer _quoteServer;
    private static RemoteServer _remoteQuoteServer;
    private static DBServer _dbServer;
    private static RemoteServer _remoteDbServer;
    */

    private class TimedVariable<T> {
        private long time;
        private String string;
        private long timeout;
        private T server;

        public TimedVariable(long timeout) {
            this.timeout = timeout;
        }

        public void setString(String val) {
            string = val;
        }

        public void setServer(T server) {
            this.server = server;
            time = System.currentTimeMillis();
        }

        public T getServer() {
            return this.server;
        }

        public String getString() {
            return string;
        }

        public boolean isExpired() {
            return time == 0 || System.currentTimeMillis() - time > timeout;
        }
    }

    private class SessionSweepThread extends Thread {
        private boolean terminated = false;
        public void terminate() { terminated = true; }
        public void run() {
            int seconds = 0;
            while (!terminated) {
                try {
                    Thread.sleep(1000);
                    if (++seconds % 120 == 0) {
                        seconds = 0;
                        synchronized (sessions) {
                            Iterator<TimedVariable<TransactionServer> > i = sessions.values().iterator();
                            while (i.hasNext()) {
                                if (i.next().isExpired()) {
                                    i.remove();
                                }
                            }
                        }
                    }
                } catch (InterruptedException ignored) {
                }
            }
        }
    }

    private static final int SERVER_CACHE_TIMEOUT_MS = 60000;

    //private TimedVariable<TransactionServer> transHostTimed = new TimedVariable<TransactionServer>(60000);
    private TimedVariable<DBServer> dbHostTimed = new TimedVariable<DBServer>(SERVER_CACHE_TIMEOUT_MS);
    private TimedVariable<QuoteServer> quoteHostTimed = new TimedVariable<QuoteServer>(SERVER_CACHE_TIMEOUT_MS);
    private TimedVariable<AuditLog> auditLogHostTimed = new TimedVariable<AuditLog>(SERVER_CACHE_TIMEOUT_MS);
    private Hashtable<Long, TimedVariable<TransactionServer> > sessions = new Hashtable<Long, TimedVariable<TransactionServer> >();

    public static int TRANSACTION_PORT = 7200;
    public static int QUOTE_PORT = 7201;
    public static int DB_PORT = 7202;
    public static int LOG_PORT = 7203;
    public static int REGISTRY_PORT = 7204;
    private static Category debugLog = Logger.getInstance("ServerFactoryDebug");

    private SessionSweepThread sessionSweepThread;

    public ServerFactory() {
        sessionSweepThread = new SessionSweepThread();
        sessionSweepThread.start();
    };

    public void dispose() {
        if (sessionSweepThread != null) {
            try {
                sessionSweepThread.terminate();
                sessionSweepThread.join();
            } catch (InterruptedException ignored) {
            }
            sessionSweepThread = null;
        }
    }

    public static ServerRegistry getServerRegistry() throws CommException {
        try {
            try {
                return getLocalServerRegistry();
            } catch (Exception err) {
            }

            ServerRegistry server = null;
            Registry registry = null;

            String host = System.getProperty("group2.factory.RegistryHost");
            if (host == null) {
                throw new CommException("group2.factory.RegistryHost is not defined.  Check server_config.sh");
            }

            debugLog.info("ServerFactory: connecting to remote registry at " + host);
            try {
                registry = LocateRegistry.getRegistry(host, REGISTRY_PORT);
                server = (ServerRegistry)registry.lookup(ServerRegistry.RMI_NAME);
                //server.startStats();
            } catch (Exception err) {
                err.printStackTrace();
                throw new CommException("ServerRegistry is not running on host " + host + ": " + err.getMessage());
            }
            return server;
        } catch (RuntimeException err) {
            debugLog.error("Failed to get ServerRegistry: RuntimeException", err);
            throw err;
        } catch (CommException err) {
            debugLog.error("Failed to get ServerRegistry: CommException", err);
            throw err;
        }
    }

    public static ServerRegistry getLocalServerRegistry() throws CommException {
        ServerRegistry server = null;
        Registry registry = null;

        try {
            debugLog.debug("Searching for ServerRegistry on localhost");
            registry = LocateRegistry.getRegistry(REGISTRY_PORT);
            server = (ServerRegistry)registry.lookup(ServerRegistry.RMI_NAME);
            debugLog.info("ServerFactory using local registry");
        } catch (Exception err) {
            debugLog.info("No ServerRegistry on localhost");
            throw new CommException("ServerRegistry is not running on localhost: " + err.getMessage());
        }
        return server;
    }

    public static ServerRegistry startServerRegistry() throws CommException {
        ServerRegistry server = null;
        Registry registry = null;
        try {
            debugLog.info("Starting ServerRegistry on localhost");
            RemoteServer remoteServer = new ServerRegistryImpl();
            remoteServer.serverStart(REGISTRY_PORT);

            registry = LocateRegistry.getRegistry(REGISTRY_PORT);
            server = (ServerRegistry)registry.lookup(ServerRegistry.RMI_NAME);
        } catch (Exception err) {
            debugLog.error("Failed to start ServerRegistry", err);
            throw new CommException("Couldn't start ServerRegistry: " + err.getMessage());
        }
        return server;
    }

    public static TransactionServer getTransactionServerByHost(String host) throws CommException {
        TransactionServer server = null;
        Registry registry = null;
        try {
            debugLog.info("Connecting to TransactionServer on " + host);
            registry = LocateRegistry.getRegistry(host, TRANSACTION_PORT);
            server = (TransactionServer)registry.lookup(TransactionServer.RMI_NAME);
        } catch (Exception err) {
            // Couldn't start server
            err.printStackTrace();
            debugLog.error("Couldn't connect to TransactionServer on host " + host, err);
            throw new CommException("Couldn't connect to TransactionServer on host " + host + ": " + err.getMessage());
        }
        return server;
    }

    public TransactionServer getTransactionServer(long sessionId) throws CommException, SessionNotFoundException {

        // If sessionId is zero, need to go to registry to get load-balanced server id
        if (sessionId == 0L) {
            TransactionServer server = null;
            Registry registry = null;
            String host = "default";
            try {
                ServerRegistry serverReg = getServerRegistry();
                host = serverReg.getTransactionServerHost(sessionId);
                if (host == null || host.length() == 0) {
                    throw new Exception("TransactionServer not registered");
                }
            } catch (Exception err) {
                // Couldn't start server
                err.printStackTrace();
                debugLog.error("Couldn't connect to TransactionServer on host " + host, err);
                throw new CommException("Couldn't connect to TransactionServer on host " + host + ": " + err.getMessage());
            }
            return getTransactionServerByHost(host);
        }

        // Otherwise, sessionId specified.  See if it's cached, otherwise get from registry
        TimedVariable<TransactionServer> transServer = null;
        Long sessionIdKey = new Long(sessionId);

        synchronized (sessions) {
            transServer = sessions.get(sessionIdKey);
            if (transServer == null) {
                transServer = new TimedVariable<TransactionServer>(SERVER_CACHE_TIMEOUT_MS);
                sessions.put(sessionIdKey, transServer);
            }
        }

        if (transServer.isExpired()) {
            synchronized (transServer) {
                if (transServer.isExpired()) {
                    TransactionServer server = null;
                    Registry registry = null;
                    String host = "default";
                    try {
                        ServerRegistry serverReg = getServerRegistry();
                        host = serverReg.getTransactionServerHost(sessionId);
                        if (host == null || host.length() == 0) {
                            throw new SessionNotFoundException(sessionId);
                        }
                        transServer.setString(host);
                    } catch (SessionNotFoundException err) {
                        debugLog.info("Attempt to use expired session id " + sessionId);
                        throw err;
                    } catch (Exception err) {
                        // Couldn't start server
                        err.printStackTrace();
                        debugLog.error("Couldn't connect to TransactionServer on host " + host, err);
                        throw new CommException("Couldn't connect to TransactionServer on host " + host + ": " + err.getMessage());
                    }

                    transServer.setServer(getTransactionServerByHost(transServer.getString()));
                }
            }
        }

        return transServer.getServer();
    }

    public static TransactionServer startTransactionServer() throws CommException {
        TransactionServer server = null;
        Registry registry = null;
        try {
            debugLog.info("Starting TransactionServer on localhost");
            ServerManager serverManager = (ServerManager)getServerRegistry();
            RemoteServer remoteServer = new TransactionServerImpl();
            remoteServer.serverStart(TRANSACTION_PORT);
            serverManager.registerServer(remoteServer, ServerManager.TRANSACTION_SERVER);

            registry = LocateRegistry.getRegistry(TRANSACTION_PORT);
            server = (TransactionServer)registry.lookup(TransactionServer.RMI_NAME);
            debugLog.debug("TransactionServer Started");
        } catch (Exception err) {
            // Couldn't start server
            debugLog.error("Couldn't start TransactionServer", err);
            throw new CommException("Couldn't start TransactionServer: " + err.getMessage());
        }
        return server;
    }

    public static QuoteServer getQuoteServerByHost(String host) throws CommException {
        QuoteServer server = null;
        Registry registry = null;
        try {
            debugLog.debug("Connecting to QuoteServer on host " + host);
            registry = LocateRegistry.getRegistry(host, QUOTE_PORT);
            server = (QuoteServer)registry.lookup(QuoteServer.RMI_NAME);
        } catch (Exception err) {
            // Couldn't start server
            debugLog.error("Couldn't connect to QuoteServer", err);
            throw new CommException("Couldn't connect to QuoteServer: " + err.getMessage());
        }
        return server;
    }

    public QuoteServer getQuoteServer() throws CommException {
        if (quoteHostTimed.isExpired()) {
            synchronized (quoteHostTimed) {
                if (quoteHostTimed.isExpired()) {
                    QuoteServer server = null;
                    Registry registry = null;
                    String host = null;
                    try {
                        ServerRegistry serverReg = getServerRegistry();
                        host = serverReg.getQuoteServerHost();
                        if (host == null || host.length() == 0) {
                            throw new Exception("QuoteServer not registered");
                        }
                        quoteHostTimed.setString(host);
                    } catch (Exception err) {
                        // Couldn't start server
                        debugLog.error("Couldn't connect to QuoteServer", err);
                        throw new CommException("Couldn't connect to QuoteServer: " + err.getMessage());
                    }
                    quoteHostTimed.setServer(getQuoteServerByHost(host));
                }
            }
        }
        return quoteHostTimed.getServer();
    }

    public static QuoteServer startQuoteServer() throws CommException {
        QuoteServer server = null;
        Registry registry = null;
        try {
            debugLog.info("Starting QuoteServer on localhost");
            ServerManager serverManager = (ServerManager)getServerRegistry();
            RemoteServer remoteServer;

            if (System.getProperty("group2.factory.QuoteStub", "false").equals("true")) {
                remoteServer = new QuoteServerStub();
            } else {
                remoteServer = new QuoteServerImpl();
            }

            remoteServer.serverStart(QUOTE_PORT);
            serverManager.registerServer(remoteServer, ServerManager.QUOTE_SERVER);

            registry = LocateRegistry.getRegistry(QUOTE_PORT);
            server = (QuoteServer)registry.lookup(QuoteServer.RMI_NAME);
            debugLog.debug("QuoteServer Started");
        } catch (Exception err) {
            // Couldn't start server
            debugLog.error("Couldn't start QuoteServer", err);
            throw new CommException("Couldn't start QuoteServer: " + err.getMessage());
        }
        return server;
    }

    public DBServer getDBServer() throws CommException {
        if (dbHostTimed.isExpired()) {
            synchronized (dbHostTimed) {
                if (dbHostTimed.isExpired()) {
                    DBServer server = null;
                    Registry registry = null;
                    String host = null;
                    try {
                        ServerRegistry serverReg = getServerRegistry();
                        host = serverReg.getDBServerHost();
                        if (host == null || host.length() == 0) {
                            throw new Exception("DB Server not registered");
                        }
                        dbHostTimed.setString(host);
                    } catch (Exception err) {
                        // Couldn't start server
                        debugLog.error("Couldn't connect to DB Server", err);
                        throw new CommException("Couldn't connect to DB Server: " + err.getMessage());
                    }
                    dbHostTimed.setServer(getDBServerByHost(host));
                }
            }
        }
        return dbHostTimed.getServer();
    }

    public static DBServer getDBServerByHost(String host) throws CommException {
        DBServer server = null;
        Registry registry = null;
        try {
            debugLog.info("Connecting to DB Server on host " + host);
            registry = LocateRegistry.getRegistry(host, DB_PORT);
            server = (DBServer)registry.lookup(DBServer.RMI_NAME);
        } catch (Exception err) {
            // Couldn't start server
            debugLog.error("Couldn't connect to DB Server", err);
            throw new CommException("Couldn't connect to DB Server: " + err.getMessage());
        }
        return server;
    }

    public static DBServer startDBServer() throws CommException {
        DBServer server = null;
        Registry registry = null;
        try {
            debugLog.info("Starting DB Server on local host");
            ServerManager serverManager = (ServerManager)getServerRegistry();
            RemoteServer remoteServer;

            if (System.getProperty("group2.factory.DBStub", "false").equals("true")) {
                remoteServer = new DBServerImplStub();
            } else {
                remoteServer = new DBServerImpl2();
            }

            remoteServer.serverStart(DB_PORT);
            serverManager.registerServer(remoteServer, ServerManager.DB_SERVER);

            registry = LocateRegistry.getRegistry(DB_PORT);
            server = (DBServer)registry.lookup(DBServer.RMI_NAME);
            debugLog.debug("DBServer Started");
        } catch (Exception err) {
            // Couldn't start server
            debugLog.error("Couldn't start DB Server", err);
            throw new CommException("Couldn't start DB Server: " + err.getMessage());
        }
        return server;
    }

    public AuditLog getAuditServer() throws CommException {
        if (auditLogHostTimed.isExpired()) {
            synchronized (auditLogHostTimed) {
                if (auditLogHostTimed.isExpired()) {
                    AuditLog server = null;
                    Registry registry = null;
                    String host = null;
                    try {
                        ServerRegistry serverReg = getServerRegistry();
                        host = serverReg.getAuditLogHost();
                        if (host == null || host.length() == 0) {
                            throw new Exception("Log Server not registered");
                        }
                        auditLogHostTimed.setString(host);
                    } catch (Exception err) {
                        // Couldn't start server
                        debugLog.error("Couldn't connect to Log Server", err);
                        throw new CommException("Couldn't connect to Log Server: " + err.getMessage());
                    }
                    auditLogHostTimed.setServer(getAuditServerByHost(host));
                }
            }
        }
        return auditLogHostTimed.getServer();
    }

    public static AuditLog getAuditServerByHost(String host) throws CommException {
        AuditLog server = null;
        Registry registry = null;
        try {
            debugLog.info("Connecting to Log server on host " + host);
            registry = LocateRegistry.getRegistry(host, LOG_PORT);
            server = (AuditLog)registry.lookup(AuditLog.RMI_NAME);
        } catch (Exception err) {
            // Couldn't start server
            debugLog.error("Couldn't connect to Log Server", err);
            throw new CommException("Couldn't connect to Log Server: " + err.getMessage());
        }
        return server;
    }

    public static AuditLog startAuditServer() throws CommException {
        AuditLog server = null;
        Registry registry = null;
        try {
            debugLog.info("Satrting AuditServer on localhost");
            ServerManager serverManager = (ServerManager)getServerRegistry();
            RemoteServer remoteServer = new AuditLogImpl();
            remoteServer.serverStart(LOG_PORT);
            serverManager.registerServer(remoteServer, ServerManager.AUDIT_LOG);

            registry = LocateRegistry.getRegistry(LOG_PORT);
            server = (AuditLog)registry.lookup(AuditLog.RMI_NAME);
            debugLog.debug("LogServer Started");
        } catch (Exception err) {
            // Couldn't start server
            debugLog.error("Couldn't start Log Server", err);
            throw new CommException("Couldn't start Log Server: " + err.getMessage());
        }
        return server;
    }

    public static void serverStart(Remote server, int port, String rmiName)
        throws RemoteException
    {
        debugLog.info("Starting " + rmiName + "...");
        Registry registry = null;

        try {
            registry = LocateRegistry.createRegistry(port);
        } catch (Exception err) {
            debugLog.debug("Failed to create reg: " + err.getMessage());
            registry = LocateRegistry.getRegistry(port);
            debugLog.debug("Got existing reg.");
        }

        registry.rebind(rmiName, server);
        debugLog.debug("Started");
    }

    public static void serverStop(int port, String rmiName) throws RemoteException,
                                                                   NotBoundException
    {
        System.out.println("Stopping " + rmiName + " on port " + port + "...");

        try {
            // Send the stop signal to the server, if it's running
            Registry registry = LocateRegistry.getRegistry(port);
            RemoteServer server = (RemoteServer)registry.lookup(rmiName);
            if (server != null) {
                server.serverStop();
                try {
                    ServerManager manager = (ServerManager)getLocalServerRegistry();
                    manager.unregisterServer(server);
                } catch (Exception ignored) {
                    // ServerManager not running - that's ok
                }
            }
        } catch (Exception err) {
            err.printStackTrace();
            System.out.println("Stop error: " + err.getMessage());
        }

        //registry.unbind(rmiName);
        System.out.println("Stopped.");
    }

    /*
    public static UserInfo createUserInfo(int id, String userName, float balance) {
        return new UserInfoImpl(id, userName, balance);
        }*/
}
